Skip to content

bugfix: Save victory status to prevent early exits from resulting in defeat in network matches#2292

Open
Stubbjax wants to merge 7 commits intoTheSuperHackers:mainfrom
Stubbjax:fix-victory-conditions
Open

bugfix: Save victory status to prevent early exits from resulting in defeat in network matches#2292
Stubbjax wants to merge 7 commits intoTheSuperHackers:mainfrom
Stubbjax:fix-victory-conditions

Conversation

@Stubbjax
Copy link

@Stubbjax Stubbjax commented Feb 11, 2026

This change fixes an issue where a player's victory is not reliably recorded at the end of a network match.

If a player quits the game at any point before the score screen, and it results in the destruction of their assets (i.e. no allies exist), then a loss is recorded, regardless of the actual outcome. This is because the victory condition is processed at the time the game ends and the score screen is shown. To resolve this, the player's victory is now saved as soon as one team remains and the match is considered over, and so any subsequent defeat + asset destruction has no effect on the recorded outcome.

The change includes a minor refactor by abstracting some of the update logic into a multipleAlliancesExist function, and consolidating some stat conditions. There are further refactors that can be done in follow-up changes, such as reading m_isDefeated instead of repeatedly calling hasSinglePlayerBeenDefeated in multiple places, and splitting several blocks in the update method into functions.

Tests

Below is a series of network match test results to verify the fix.

Test 1

Knife surrenders at the start of a 1v1 match. Fork then quits the game.

Player Team Old Outcome New Outcome
Knife - Loss Loss
Fork - Loss Win*

Test 2

Knife surrenders at the start of a 1v1v1 match. Fork then surrenders. Spoon then quits the game.

Player Team Old Outcome New Outcome
Knife - Loss Loss
Fork - Loss Loss
Spoon - Loss Win*

Test 3

Knife surrenders at the start of a 2v1 match. Fork then surrenders. Spoon then quits the game.

Player Team Old Outcome New Outcome
Knife 1 Loss Win*
Fork - Loss Loss
Spoon 1 Loss Win*

Test 4

Fork surrenders at the start of a 2v1 match. Spoon then quits the game. Knife then quits the game.

Player Team Old Outcome New Outcome
Knife 1 Loss Win*
Fork - Loss Loss
Spoon 1 Win Win

Test 5

Knife surrenders at the start of a 2v1 match. Spoon then surrenders. Fork then quits the game.

Player Team Old Outcome New Outcome
Knife 1 Loss Loss
Fork - Loss Win*
Spoon 1 Loss Loss

@Stubbjax Stubbjax self-assigned this Feb 11, 2026
@Stubbjax Stubbjax added Bug Something is not working right, typically is user facing Minor Severity: Minor < Major < Critical < Blocker Gen Relates to Generals ZH Relates to Zero Hour Refactor Edits the code with insignificant behavior changes, is never user facing labels Feb 11, 2026
@greptile-apps
Copy link

greptile-apps bot commented Feb 11, 2026

Greptile Summary

This PR fixes a bug where players who quit before the score screen would incorrectly receive a loss even if their team won. The fix caches victory status in a new m_isVictorious array as soon as one alliance remains, preventing subsequent asset destruction from affecting the outcome.

Key Changes:

  • Added m_isVictorious array to cache victory status when match ends
  • Refactored alliance checking into multipleAlliancesExist() helper function
  • Modified hasAchievedVictory() and hasBeenDefeated() to consult cached status arrays
  • ScoreScreen.cpp: Consolidated duplicate stat-tracking code for cleaner structure

Critical Issue:
The previous review comment about multipleAlliancesExist() returning false for both "one alliance alive" and "zero alliances alive" remains valid and unaddressed. While the code handles this case (via the if (victoriousPlayer) null check on line 194), the function name and logic suggest it should explicitly distinguish these cases. In simultaneous-defeat scenarios, no one would be marked victorious, which may be the intended behavior for draws, but this should be explicitly documented or handled.

Confidence Score: 4/5

  • This PR is safe to merge with one critical logic issue that should be addressed
  • The core fix correctly caches victory status to prevent early exits from voiding wins. The ScoreScreen refactoring is clean. However, there's a critical edge case in multipleAlliancesExist() that doesn't distinguish between "one alliance alive" and "zero alliances alive", which was flagged in previous review threads and remains unaddressed
  • Pay close attention to both VictoryConditions.cpp files - the multipleAlliancesExist() function needs to handle the zero-players-alive edge case

Important Files Changed

Filename Overview
Generals/Code/GameEngine/Source/GameLogic/ScriptEngine/VictoryConditions.cpp Adds victory status caching to prevent early exits from voiding wins; refactors alliance checking into multipleAlliancesExist()
GeneralsMD/Code/GameEngine/Source/GameLogic/ScriptEngine/VictoryConditions.cpp Same changes as Generals version; has minor indentation inconsistency on line 97

Flowchart

flowchart TD
    A[VictoryConditions::update called] --> B{Is multiplayer?}
    B -->|No| Z[Return]
    B -->|Yes| C{Single alliance<br/>remaining?}
    C -->|Yes| D[Check for player<br/>eliminations]
    C -->|No| E[Call multipleAlliancesExist]
    E --> F{Multiple alliances<br/>alive?}
    F -->|Yes| D
    F -->|No| G[Set singleAllianceRemaining = true<br/>Set endFrame]
    G --> H[Call findFirstVictoriousPlayer]
    H --> I{Found victorious<br/>player?}
    I -->|Yes| J[Call markAllianceVictorious<br/>Set m_isVictorious array]
    I -->|No| K[No one marked victorious]
    J --> D
    K --> D
    D --> L[For each player:<br/>Check hasSinglePlayerBeenDefeated]
    L --> M{Player defeated<br/>and not yet marked?}
    M -->|Yes| N[Set m_isDefeated<br/>Reveal map, notify, kill units]
    M -->|No| O[Continue]
    N --> O
    O --> P[Check local player defeat]
    P --> Z
Loading

Last reviewed commit: e297db3

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

@greptile-apps
Copy link

greptile-apps bot commented Feb 11, 2026

Additional Comments (2)

Generals/Code/GameEngine/Source/GameLogic/ScriptEngine/VictoryConditions.cpp
logic issue: losing players who quit early without asset destruction won't be marked as defeated

The old logic returned true for any player when m_singleAllianceRemaining && !hasAchievedVictory(player). The new logic only checks m_isDefeated[i], which is only set when hasSinglePlayerBeenDefeated() returns true (assets destroyed). If a losing player quits early and their assets are maintained by allies, they won't be marked defeated.

Add fallback logic:

Bool VictoryConditions::hasBeenDefeated(Player *player)
{
	if (!player)
		return false;

	if (!m_singleAllianceRemaining)
		return false;

	for (Int i = 0; i < MAX_PLAYER_COUNT; ++i)
	{
		if (player == m_players[i])
		{
			if (m_isDefeated[i])
				return true;
			if (!m_isVictorious[i])
				return true;
			return false;
		}
	}

	return false;
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: Generals/Code/GameEngine/Source/GameLogic/ScriptEngine/VictoryConditions.cpp
Line: 287:302

Comment:
logic issue: losing players who quit early without asset destruction won't be marked as defeated

The old logic returned true for any player when `m_singleAllianceRemaining && !hasAchievedVictory(player)`. The new logic only checks `m_isDefeated[i]`, which is only set when `hasSinglePlayerBeenDefeated()` returns true (assets destroyed). If a losing player quits early and their assets are maintained by allies, they won't be marked defeated.

Add fallback logic:

```cpp
Bool VictoryConditions::hasBeenDefeated(Player *player)
{
	if (!player)
		return false;

	if (!m_singleAllianceRemaining)
		return false;

	for (Int i = 0; i < MAX_PLAYER_COUNT; ++i)
	{
		if (player == m_players[i])
		{
			if (m_isDefeated[i])
				return true;
			if (!m_isVictorious[i])
				return true;
			return false;
		}
	}

	return false;
}
```

How can I resolve this? If you propose a fix, please make it concise.

GeneralsMD/Code/GameEngine/Source/GameLogic/ScriptEngine/VictoryConditions.cpp
logic issue: losing players who quit early without asset destruction won't be marked as defeated

The old logic returned true for any player when m_singleAllianceRemaining && !hasAchievedVictory(player). The new logic only checks m_isDefeated[i], which is only set when hasSinglePlayerBeenDefeated() returns true (assets destroyed). If a losing player quits early and their assets are maintained by allies, they won't be marked defeated.

Add fallback logic:

Bool VictoryConditions::hasBeenDefeated(Player *player)
{
	if (!player)
		return false;

	if (!m_singleAllianceRemaining)
		return false;

	for (Int i = 0; i < MAX_PLAYER_COUNT; ++i)
	{
		if (player == m_players[i])
		{
			if (m_isDefeated[i])
				return true;
			if (!m_isVictorious[i])
				return true;
			return false;
		}
	}

	return false;
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: GeneralsMD/Code/GameEngine/Source/GameLogic/ScriptEngine/VictoryConditions.cpp
Line: 289:304

Comment:
logic issue: losing players who quit early without asset destruction won't be marked as defeated

The old logic returned true for any player when `m_singleAllianceRemaining && !hasAchievedVictory(player)`. The new logic only checks `m_isDefeated[i]`, which is only set when `hasSinglePlayerBeenDefeated()` returns true (assets destroyed). If a losing player quits early and their assets are maintained by allies, they won't be marked defeated.

Add fallback logic:

```cpp
Bool VictoryConditions::hasBeenDefeated(Player *player)
{
	if (!player)
		return false;

	if (!m_singleAllianceRemaining)
		return false;

	for (Int i = 0; i < MAX_PLAYER_COUNT; ++i)
	{
		if (player == m_players[i])
		{
			if (m_isDefeated[i])
				return true;
			if (!m_isVictorious[i])
				return true;
			return false;
		}
	}

	return false;
}
```

How can I resolve this? If you propose a fix, please make it concise.

@Stubbjax Stubbjax force-pushed the fix-victory-conditions branch from e688101 to 87a75a4 Compare February 11, 2026 06:52
Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines 146 to 148
Bool VictoryConditions::multipleAlliancesExist()
{
Player* alive = nullptr;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incorrect win detection
multipleAlliancesExist() returns false when zero non-defeated players exist (e.g., simultaneous elimination / edge cases where everyone is defeated). In that case update() will set m_singleAllianceRemaining = true and cache a “victorious” player based on the first non-defeated player found, but there won’t be one, so no one gets marked victorious. This makes hasAchievedVictory() always return false, and can also prevent hasBeenDefeated() from ever returning true (since it now only consults m_isDefeated[i], not “not victorious”), leaving the match outcome inconsistent.

This function needs to distinguish “exactly one alliance alive” from “nobody alive” (and handle the latter explicitly).

Prompt To Fix With AI
This is a comment left during a code review.
Path: Generals/Code/GameEngine/Source/GameLogic/ScriptEngine/VictoryConditions.cpp
Line: 146:148

Comment:
**Incorrect win detection**
`multipleAlliancesExist()` returns `false` when *zero* non-defeated players exist (e.g., simultaneous elimination / edge cases where everyone is defeated). In that case `update()` will set `m_singleAllianceRemaining = true` and cache a “victorious” player based on the first non-defeated player found, but there won’t be one, so no one gets marked victorious. This makes `hasAchievedVictory()` always return false, and can also prevent `hasBeenDefeated()` from ever returning true (since it now only consults `m_isDefeated[i]`, not “not victorious”), leaving the match outcome inconsistent.

This function needs to distinguish “exactly one alliance alive” from “nobody alive” (and handle the latter explicitly).

How can I resolve this? If you propose a fix, please make it concise.

(player == m_players[i] || areAllies(m_players[i], player)) )
return true;
}
if (player == m_players[i] && m_isVictorious[i])
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optimization:

if (player == m_players[i])
{
  if (m_isVictorious[i])
    return true;
  break;
}

Same in VictoryConditions::hasBeenDefeated

// TheSuperHackers @bugfix Stubbjax 11/02/2026 Cache victory status so that premature exits don't void the victory.

Player* victoriousPlayer = nullptr;
for (Int i = 0; i < MAX_PLAYER_COUNT; ++i)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest put this block into a new function findVictoriousPlayer


if (victoriousPlayer)
{
for (Int i = 0; i < MAX_PLAYER_COUNT; ++i)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest put this block into a new function saveVictoriousStateForPlayerAndAllies(Player *player)

{
if (alive)
Player* player = m_players[i];
if (player && !hasSinglePlayerBeenDefeated(player))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be nice to put a comment explaining the logic behind this. It may not be obvious to all readers why we need the winner and then apply the win to his allies.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Bug Something is not working right, typically is user facing Gen Relates to Generals Minor Severity: Minor < Major < Critical < Blocker Refactor Edits the code with insignificant behavior changes, is never user facing ZH Relates to Zero Hour

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants